// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package qunar.tc.decompiler.struct.match;

import qunar.tc.decompiler.modules.decompiler.exps.ExitExprent;
import qunar.tc.decompiler.modules.decompiler.exps.Exprent;
import qunar.tc.decompiler.modules.decompiler.exps.FunctionExprent;
import qunar.tc.decompiler.modules.decompiler.stats.IfStatement;
import qunar.tc.decompiler.modules.decompiler.stats.Statement;
import qunar.tc.decompiler.struct.gen.VarType;
import qunar.tc.decompiler.struct.match.IMatchable.MatchProperties;

import java.util.*;

public class MatchEngine {
    private static final Map<String, MatchProperties> stat_properties = new HashMap<>();
    private static final Map<String, MatchProperties> expr_properties = new HashMap<>();
    private static final Map<String, Integer> stat_type = new HashMap<>();
    private static final Map<String, Integer> expr_type = new HashMap<>();
    private static final Map<String, Integer> expr_func_type = new HashMap<>();
    private static final Map<String, Integer> expr_exit_type = new HashMap<>();
    private static final Map<String, Integer> stat_if_type = new HashMap<>();
    private static final Map<String, VarType> expr_const_type = new HashMap<>();

    static {
        stat_properties.put("type", MatchProperties.STATEMENT_TYPE);
        stat_properties.put("ret", MatchProperties.STATEMENT_RET);
        stat_properties.put("position", MatchProperties.STATEMENT_POSITION);
        stat_properties.put("statsize", MatchProperties.STATEMENT_STATSIZE);
        stat_properties.put("exprsize", MatchProperties.STATEMENT_EXPRSIZE);
        stat_properties.put("iftype", MatchProperties.STATEMENT_IFTYPE);

        expr_properties.put("type", MatchProperties.EXPRENT_TYPE);
        expr_properties.put("ret", MatchProperties.EXPRENT_RET);
        expr_properties.put("position", MatchProperties.EXPRENT_POSITION);
        expr_properties.put("functype", MatchProperties.EXPRENT_FUNCTYPE);
        expr_properties.put("exittype", MatchProperties.EXPRENT_EXITTYPE);
        expr_properties.put("consttype", MatchProperties.EXPRENT_CONSTTYPE);
        expr_properties.put("constvalue", MatchProperties.EXPRENT_CONSTVALUE);
        expr_properties.put("invclass", MatchProperties.EXPRENT_INVOCATION_CLASS);
        expr_properties.put("signature", MatchProperties.EXPRENT_INVOCATION_SIGNATURE);
        expr_properties.put("parameter", MatchProperties.EXPRENT_INVOCATION_PARAMETER);
        expr_properties.put("index", MatchProperties.EXPRENT_VAR_INDEX);
        expr_properties.put("name", MatchProperties.EXPRENT_FIELD_NAME);

        stat_type.put("if", Statement.TYPE_IF);
        stat_type.put("do", Statement.TYPE_DO);
        stat_type.put("switch", Statement.TYPE_SWITCH);
        stat_type.put("trycatch", Statement.TYPE_TRYCATCH);
        stat_type.put("basicblock", Statement.TYPE_BASICBLOCK);
        stat_type.put("sequence", Statement.TYPE_SEQUENCE);

        expr_type.put("array", Exprent.EXPRENT_ARRAY);
        expr_type.put("assignment", Exprent.EXPRENT_ASSIGNMENT);
        expr_type.put("constant", Exprent.EXPRENT_CONST);
        expr_type.put("exit", Exprent.EXPRENT_EXIT);
        expr_type.put("field", Exprent.EXPRENT_FIELD);
        expr_type.put("function", Exprent.EXPRENT_FUNCTION);
        expr_type.put("if", Exprent.EXPRENT_IF);
        expr_type.put("invocation", Exprent.EXPRENT_INVOCATION);
        expr_type.put("monitor", Exprent.EXPRENT_MONITOR);
        expr_type.put("new", Exprent.EXPRENT_NEW);
        expr_type.put("switch", Exprent.EXPRENT_SWITCH);
        expr_type.put("var", Exprent.EXPRENT_VAR);
        expr_type.put("annotation", Exprent.EXPRENT_ANNOTATION);
        expr_type.put("assert", Exprent.EXPRENT_ASSERT);

        expr_func_type.put("eq", FunctionExprent.FUNCTION_EQ);

        expr_exit_type.put("return", ExitExprent.EXIT_RETURN);
        expr_exit_type.put("throw", ExitExprent.EXIT_THROW);

        stat_if_type.put("if", IfStatement.IFTYPE_IF);
        stat_if_type.put("ifelse", IfStatement.IFTYPE_IFELSE);

        expr_const_type.put("null", VarType.VARTYPE_NULL);
        expr_const_type.put("string", VarType.VARTYPE_STRING);
    }

    private final MatchNode rootNode;
    private final Map<String, Object> variables = new HashMap<>();

    public MatchEngine(String description) {
        // each line is a separate statement/exprent
        String[] lines = description.split("\n");

        int depth = 0;
        LinkedList<MatchNode> stack = new LinkedList<>();

        for (String line : lines) {
            List<String> properties = new ArrayList<>(Arrays.asList(line.split("\\s+"))); // split on any number of whitespaces
            if (properties.get(0).isEmpty()) {
                properties.remove(0);
            }

            int node_type = "statement".equals(properties.get(0)) ? MatchNode.MATCHNODE_STATEMENT : MatchNode.MATCHNODE_EXPRENT;

            // create new node
            MatchNode matchNode = new MatchNode(node_type);
            for (int i = 1; i < properties.size(); ++i) {
                String[] values = properties.get(i).split(":");

                MatchProperties property = (node_type == MatchNode.MATCHNODE_STATEMENT ? stat_properties : expr_properties).get(values[0]);
                if (property == null) { // unknown property defined
                    throw new RuntimeException("Unknown matching property");
                } else {
                    Object value;
                    int parameter = 0;

                    String strValue = values[1];
                    if (values.length == 3) {
                        parameter = Integer.parseInt(values[1]);
                        strValue = values[2];
                    }

                    switch (property) {
                        case STATEMENT_TYPE:
                            value = stat_type.get(strValue);
                            break;
                        case STATEMENT_STATSIZE:
                        case STATEMENT_EXPRSIZE:
                            value = Integer.valueOf(strValue);
                            break;
                        case STATEMENT_POSITION:
                        case EXPRENT_POSITION:
                        case EXPRENT_INVOCATION_CLASS:
                        case EXPRENT_INVOCATION_SIGNATURE:
                        case EXPRENT_INVOCATION_PARAMETER:
                        case EXPRENT_VAR_INDEX:
                        case EXPRENT_FIELD_NAME:
                        case EXPRENT_CONSTVALUE:
                        case STATEMENT_RET:
                        case EXPRENT_RET:
                            value = strValue;
                            break;
                        case STATEMENT_IFTYPE:
                            value = stat_if_type.get(strValue);
                            break;
                        case EXPRENT_FUNCTYPE:
                            value = expr_func_type.get(strValue);
                            break;
                        case EXPRENT_EXITTYPE:
                            value = expr_exit_type.get(strValue);
                            break;
                        case EXPRENT_CONSTTYPE:
                            value = expr_const_type.get(strValue);
                            break;
                        case EXPRENT_TYPE:
                            value = expr_type.get(strValue);
                            break;
                        default:
                            throw new RuntimeException("Unhandled matching property");
                    }

                    matchNode.addRule(property, new MatchNode.RuleValue(parameter, value));
                }
            }

            if (stack.isEmpty()) { // first line, root node
                stack.push(matchNode);
            } else {
                // return to the correct parent on the stack
                int new_depth = line.lastIndexOf(' ', depth) + 1;
                for (int i = new_depth; i <= depth; ++i) {
                    stack.pop();
                }

                // insert new node
                stack.getFirst().addChild(matchNode);
                stack.push(matchNode);

                depth = new_depth;
            }
        }

        this.rootNode = stack.getLast();
    }

    public boolean match(IMatchable object) {
        variables.clear();
        return match(this.rootNode, object);
    }

    private boolean match(MatchNode matchNode, IMatchable object) {
        if (!object.match(matchNode, this)) {
            return false;
        }

        int expr_index = 0;
        int stat_index = 0;
        for (MatchNode childNode : matchNode.getChildren()) {
            boolean isStatement = childNode.getType() == MatchNode.MATCHNODE_STATEMENT;

            IMatchable childObject = object.findObject(childNode, isStatement ? stat_index : expr_index);
            if (childObject == null || !match(childNode, childObject)) {
                return false;
            }

            if (isStatement) {
                stat_index++;
            } else {
                expr_index++;
            }
        }

        return true;
    }

    public boolean checkAndSetVariableValue(String name, Object value) {
        Object old_value = variables.get(name);
        if (old_value != null) {
            return old_value.equals(value);
        } else {
            variables.put(name, value);
            return true;
        }
    }

    public Object getVariableValue(String name) {
        return variables.get(name);
    }
}